use std::ops::Deref;

use crate::*;

use syn::{
    braced, parenthesized,
    parse::{Parse, ParseStream},
    spanned::Spanned,
    Error as ParseError, Ident, ReturnType, Token, Type, Visibility,
};

impl Parse for ServiceMacroOption {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let generate_which_side = input.parse::<Ident>()?;
        let (server_side, client_side) = match &*generate_which_side.to_string() {
            "both_sides" => (true, true),
            "client" => (false, true),
            "server" => (true, false),
            _ => {
                return Err(ParseError::new(
                    generate_which_side.span(),
                    r#"Should be one of "both_sides", "server" and "client""#,
                ))
            }
        };
        let mut static_ref_deep_down = (false, false);
        if !input.is_empty() {
            input.parse::<Token![,]>()?;
            let static_ref_under_surface = input.parse::<Ident>()?;
            if static_ref_under_surface.ne("static_ref_all_under_surface_solve") {
                return Err(ParseError::new(
                    static_ref_under_surface.span(),
                    "this option should only be \"static_ref_all_under_surface_solve\"",
                ));
            }
            let within_paren;
            parenthesized!(within_paren in input);
            let req_or_resp = within_paren.parse::<Ident>()?;
            static_ref_deep_down = match &*req_or_resp.to_string() {
                "both" => (true, true),
                "req" => (true, false),
                "resp" => (false, true),
                _ => {
                    return Err(ParseError::new(
                        req_or_resp.span(),
                        r#"Should be one of "both", "req" and "resp""#,
                    ))
                }
            };
        }
        Ok(Self {
            server_side,
            client_side,
            static_ref_deep_down,
        })
    }
}

// vis "trait" ident "{"
//     List[RpcProcedure]
// "}"
impl Parse for RpcServiceTrait {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let vis = input.parse::<Visibility>()?;
        input.parse::<Token![trait]>()?;
        let name = input.parse::<Ident>()?;

        let within_brace;
        braced!(within_brace in input);

        let mut fns = Vec::new();
        while !within_brace.is_empty() {
            fns.push(within_brace.parse::<RpcProcedure>()?);
        }

        Ok(Self { vis, name, fns })
    }
}

macro_rules! allow_ref_only_static {
    ($typ: expr) => {
        if let Type::Reference(ref_typ) = $typ {
            let allowed = ref_typ
                .lifetime
                .as_ref()
                .is_some_and(|it| it.ident.eq("static"));
            if !allowed {
                return Err(ParseError::new(
                    ref_typ.span(),
                    "Currently only static references are allowed!",
                ));
            }
        }
    };
}

// "async" "fn" ident "(" "&" "self" "," ident ":" type "," "ident" ":" "type" "," ... ")" ReturnType ";"
impl Parse for RpcProcedure {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        input.parse::<Token!(async)>()?;
        input.parse::<Token!(fn)>()?;
        let fn_name = input.parse::<Ident>()?;

        let within_paren;
        parenthesized!(within_paren in input);

        within_paren.parse::<Token![&]>()?;
        within_paren.parse::<Token![self]>()?;

        let mut args = Vec::new();
        while !within_paren.is_empty() {
            within_paren.parse::<Token![,]>()?;
            // permitting trailling comma:
            if within_paren.is_empty() {
                break;
            }

            let arg_name = within_paren.parse::<Ident>()?;
            within_paren.parse::<Token![:]>()?;
            let arg_typ = within_paren.parse::<Box<Type>>()?;
            allow_ref_only_static!(&*arg_typ);
            args.push(TypedIdent(arg_name, arg_typ));
        }

        let ret_type = RetType::from(input.parse::<ReturnType>()?);
        if let RetType::Type(ref typ) = ret_type {
            allow_ref_only_static!(typ.deref());
        }
        input.parse::<Token!(;)>()?;

        Ok(Self {
            fn_name,
            args,
            ret_type,
        })
    }
}
